import VueApollo from 'vue-apollo';
import {
  GlDrawer,
  GlSkeletonLoader,
  GlAlert,
  GlIcon,
  GlBadge,
  GlSprintf,
  GlButton,
} from '@gitlab/ui';
import Vue from 'vue';
import { createMockSubscription } from 'mock-apollo-client';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ExplainVulnerabilityDrawer from 'ee/vulnerabilities/components/explain_vulnerability/explain_vulnerability_drawer.vue';
import ExplainVulnerabilityUserFeedback from 'ee/vulnerabilities/components/explain_vulnerability/explain_vulnerability_user_feedback.vue';
import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql';
import aiActionMutation from 'ee/graphql_shared/mutations/ai_action.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getMarkdown } from '~/rest_api';

Vue.use(VueApollo);

const MOCK_VULNERABILITY = { id: 1 };
const RESPONSE_TEXT = 'response text';
const SUBSCRIPTION_RESPONSE = {
  responseBody: RESPONSE_TEXT,
  errors: [],
  requestId: '123',
  role: 'assistant',
  timestamp: '2021-05-26T14:00:00.000Z',
  type: null,
  chunkId: null,
};
const SUBSCRIPTION_ERROR_RESPONSE = { ...SUBSCRIPTION_RESPONSE, errors: ['subscription error'] };

jest.mock('~/rest_api', () => ({
  getMarkdown: jest.fn().mockResolvedValue({ data: { html: RESPONSE_TEXT } }),
}));

jest.mock('~/lib/utils/dom_utils', () => ({
  getContentWrapperHeight: () => '123px',
}));

describe('Explain Vulnerability Drawer component', () => {
  let wrapper;
  // mockSubscription is used to send subscription messages, subscriptionSpy is used to verify that the mock
  // subscription was called.
  let mockSubscription;
  let subscriptionSpy;

  const MUTATION_DEFAULT_RESPONSE = jest.fn().mockResolvedValue({
    data: { aiAction: { errors: [] } },
  });
  const MUTATION_GLOBAL_ERROR = jest.fn().mockResolvedValue({
    data: { aiAction: null },
    errors: [{ message: 'mutation global error' }],
  });
  const MUTATION_AI_ACTION_ERROR = jest.fn().mockResolvedValue({
    data: { aiAction: { errors: ['mutation ai action error'] } },
  });

  const createWrapper = ({
    mutationResponse = MUTATION_DEFAULT_RESPONSE,
    includeSourceCode = false,
  } = {}) => {
    mockSubscription = createMockSubscription();
    subscriptionSpy = jest.fn().mockReturnValue(mockSubscription);

    const apolloProvider = createMockApollo([[aiActionMutation, mutationResponse]]);
    apolloProvider.defaultClient.setRequestHandler(aiResponseSubscription, subscriptionSpy);

    wrapper = shallowMountExtended(ExplainVulnerabilityDrawer, {
      apolloProvider,
      propsData: { isOpen: false, vulnerability: MOCK_VULNERABILITY, includeSourceCode },
      stubs: { GlDrawer, GlSprintf, GlButton },
    });
  };

  const sendSubscriptionMessage = (aiCompletionResponse) => {
    mockSubscription.next({ data: { aiCompletionResponse } });
    return waitForPromises();
  };

  const createWrapperAndOpenDrawer = (params) => {
    createWrapper(params);
    return wrapper.setProps({ isOpen: true });
  };

  const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
  const findUserFeedback = () => wrapper.findComponent(ExplainVulnerabilityUserFeedback);
  const findErrorAlert = () => wrapper.findComponent(GlAlert);
  const findDrawer = () => wrapper.findComponent(GlDrawer);
  const findMarkdownDiv = () => wrapper.findByTestId('markdown');

  beforeEach(() => {
    gon.current_user_id = 1;
  });

  describe('Explain Vulnerability Drawer component', () => {
    it('does not run mutation or subscription when drawer is closed', async () => {
      createWrapper();
      await waitForPromises();

      expect(MUTATION_DEFAULT_RESPONSE).not.toHaveBeenCalled();
      expect(subscriptionSpy).not.toHaveBeenCalled();
    });

    describe('when drawer is open', () => {
      beforeEach(() => createWrapperAndOpenDrawer());

      it('shows the drawer with the expected props', () => {
        expect(findDrawer().props()).toMatchObject({
          headerHeight: '123px',
          headerSticky: true,
          zIndex: DRAWER_Z_INDEX,
        });
      });

      it('shows the tanuki-ai icon', () => {
        expect(findDrawer().findComponent(GlIcon).props('name')).toBe('tanuki-ai');
      });

      it('shows the drawer title', () => {
        expect(findDrawer().text()).toContain('Explain this vulnerability');
      });

      it('shows the maturity badge', () => {
        const badge = wrapper.findComponent(GlBadge);

        expect(badge.text()).toBe('Beta');
        expect(badge.props()).toMatchObject({
          variant: 'neutral',
          size: 'sm',
        });
      });

      it('shows the drawer subtitle', () => {
        expect(findDrawer().text()).toContain('Response generated by AI');
      });

      it('emits the close event when the drawer is closed', () => {
        findDrawer().vm.$emit('close');

        expect(wrapper.emitted('close')).toHaveLength(1);
      });
    });

    describe('AI request mutation and response subscription', () => {
      it('starts the subscription, waits for the subscription to be ready, then runs the mutation', async () => {
        await createWrapperAndOpenDrawer();
        // Subscription should be started immediately when the drawer is opened, but not the mutation.
        expect(subscriptionSpy).toHaveBeenCalled();
        expect(MUTATION_DEFAULT_RESPONSE).not.toHaveBeenCalled();
        // Send subscription ready message.
        await sendSubscriptionMessage(null);
        // After the subscription is ready, then the mutation should be called.
        expect(MUTATION_DEFAULT_RESPONSE).toHaveBeenCalled();
      });

      it('unsubscribes after it receives the AI response', async () => {
        await createWrapperAndOpenDrawer();
        // Send an AI response message, which will unsubscribe the subscription.
        await sendSubscriptionMessage(SUBSCRIPTION_RESPONSE);
        // Check that getMarkdown was called, which indirectly confirms that the subscription was unsubscribed.
        expect(getMarkdown).toHaveBeenCalledTimes(1);
        // Send the subscription ready message again.
        await sendSubscriptionMessage(SUBSCRIPTION_RESPONSE);
        // getMarkdown shouldn't be called a second time, confirming that the subscription is unsubscribed.
        expect(getMarkdown).toHaveBeenCalledTimes(1);
      });

      it('unsubscribes if the drawer is closed', async () => {
        await createWrapperAndOpenDrawer();
        // Close the drawer. This will also unsubscribe.
        await wrapper.setProps({ isOpen: false });
        // Send the subscription ready message.
        await sendSubscriptionMessage(SUBSCRIPTION_RESPONSE);
        // getMarkdown shouldn't be called, confirming that the subscription is unsubscribed.
        expect(getMarkdown).not.toHaveBeenCalled();
      });

      it('shows only the skeleton loader when loading', async () => {
        await createWrapperAndOpenDrawer();

        expect(findSkeletonLoader().exists()).toBe(true);
        expect(findErrorAlert().exists()).toBe(false);
        expect(findMarkdownDiv().exists()).toBe(false);
      });

      it.each`
        type                    | mutationResponse             | subscriptionMessage            | expectedError
        ${'mutation global'}    | ${MUTATION_GLOBAL_ERROR}     | ${null}                        | ${'Error: mutation global error'}
        ${'mutation ai action'} | ${MUTATION_AI_ACTION_ERROR}  | ${null}                        | ${'mutation ai action error'}
        ${'subscription'}       | ${MUTATION_DEFAULT_RESPONSE} | ${SUBSCRIPTION_ERROR_RESPONSE} | ${'subscription error'}
      `(
        'unsubscribes and shows only an error when there is a $type error',
        async ({ mutationResponse, subscriptionMessage, expectedError }) => {
          await createWrapperAndOpenDrawer({ mutationResponse });
          // Send subscription ready message, which also waits for the mutation to resolve.
          await sendSubscriptionMessage(null);
          // Send AI response message.
          await sendSubscriptionMessage(subscriptionMessage);

          expect(findSkeletonLoader().exists()).toBe(false);
          expect(findMarkdownDiv().exists()).toBe(false);
          expect(findErrorAlert().text()).toBe(expectedError);
          expect(findErrorAlert().props()).toMatchObject({
            variant: 'danger',
            dismissible: false,
          });
        },
      );

      it('shows the AI response', async () => {
        await createWrapperAndOpenDrawer();
        await sendSubscriptionMessage(SUBSCRIPTION_RESPONSE);

        expect(findSkeletonLoader().exists()).toBe(false);
        expect(findErrorAlert().exists()).toBe(false);
        expect(findMarkdownDiv().text()).toBe(RESPONSE_TEXT);
      });

      it.each([true, false])(
        'calls the mutation with includeSourceCode: %s',
        async (includeSourceCode) => {
          await createWrapperAndOpenDrawer({ includeSourceCode });
          await sendSubscriptionMessage(null);

          expect(MUTATION_DEFAULT_RESPONSE).toHaveBeenCalledWith({
            input: {
              explainVulnerability: {
                includeSourceCode,
                resourceId: 'gid://gitlab/Vulnerability/1',
              },
            },
          });
        },
      );
    });

    describe('User feedback component', () => {
      it('shows the user feedback with the expected prop', async () => {
        await createWrapperAndOpenDrawer();
        await sendSubscriptionMessage(SUBSCRIPTION_RESPONSE);

        expect(findUserFeedback().props('vulnerability')).toBe(MOCK_VULNERABILITY);
      });

      it('does not show the user feedback when the response is loading', async () => {
        await createWrapperAndOpenDrawer();

        expect(findSkeletonLoader().exists()).toBe(true);
        expect(findUserFeedback().exists()).toBe(false);
      });

      it.each`
        type                    | mutationResponse             | subscriptionMessage
        ${'mutation global'}    | ${MUTATION_GLOBAL_ERROR}     | ${null}
        ${'mutation ai action'} | ${MUTATION_AI_ACTION_ERROR}  | ${null}
        ${'subscription'}       | ${MUTATION_DEFAULT_RESPONSE} | ${SUBSCRIPTION_ERROR_RESPONSE}
      `(
        'does not show the user feedback when there is a $type error',
        async ({ mutationResponse, subscriptionMessage }) => {
          await createWrapperAndOpenDrawer({ mutationResponse });
          await sendSubscriptionMessage(subscriptionMessage);

          expect(findErrorAlert().exists()).toBe(true);
          expect(findUserFeedback().exists()).toBe(false);
        },
      );
    });
  });
});
